import base64
import datetime
import functools
import hashlib
import hmac
import json
import logging
import random
import string
import time
import zlib

from google.appengine.api import memcache
from google.appengine.api import users

from lib import cache
from lib import models

import config


# Auth model:
# All requests authed from a token in the Auth-Token request header.
# Each response contains an Auth-Token header that should be used for
# future requests.

# XSRF protection depends on the fact that API calls fall into two disjoint
# buckets:
# 1) Those that can mutate internal state (create channels, change the
#    playlist, merge accounts)
# 2) Those that can provide a new token

class Error(Exception): pass

class SignatureMismatch(Error): pass

class TokenExpired(Error): pass


_KEY_CHARS = string.ascii_letters + string.digits
_KEY_LENGTH = 64

def GetAuthKey():
  auth_key = None
  for key in cache.Query(models.AuthKey).filter('live =', True):
    auth_key = key.auth_key
  if auth_key:
    return auth_key
  auth_key = ''.join(random.choice(_KEY_CHARS) for _ in xrange(_KEY_LENGTH))
  models.AuthKey(auth_key=auth_key).put()
  return auth_key


def GenerateToken(account):
  contents = {
    'account': str(account.key()),
    'expires': int(time.time()) + config.SESSION_IDLE_SECONDS,
  }
  contents_str = zlib.compress(json.dumps(contents))
  sig = hmac.new(GetAuthKey(), contents_str, hashlib.sha512)
  return '%s:%s' % (base64.b64encode(contents_str), sig.hexdigest())


def ParseToken(token):
  contents_str_b64, user_sig = token.split(':', 1)
  contents_str = base64.b64decode(contents_str_b64)
  valid = False
  for key in cache.Query(models.AuthKey):
    local_sig = hmac.new(key.auth_key, contents_str, hashlib.sha512).hexdigest()
    if user_sig == local_sig:
      valid = True
  if not valid:
    raise SignatureMismatch
  contents = json.loads(zlib.decompress(contents_str))
  if contents['expires'] < time.time():
    raise TokenExpired
  return contents


def account_required(handler):

  @functools.wraps(handler)
  def Authenticate(self):
    user_token = self.request.headers.get('Auth-Token')
    if not user_token:
      self.error(403)
      self.response.out.write('Auth-Token header required')
      return
    contents = ParseToken(user_token)
    self.account = models.Account.get(contents['account'])
    return handler(self)

  return Authenticate


